﻿Option Explicit On
Option Strict On

Imports EnvDTE80
Imports Microsoft.TeamFoundation.Client
Imports Microsoft.TeamFoundation.Server
Imports Microsoft.TeamFoundation.VersionControl.Client
Imports System.IO
Imports System.Security.Principal
Imports System.Threading


Public Class ProjectProvisioner
   Public Enum ProvisionProgressEventType
      ConnectToTfsClient
      ConnectToTfsServer
      BeginProject
      EndProject
      CreateProjectDefinition
      CreateTeamProject
      RemoveGroupMember
      AddGroupMember
   End Enum

   Public Event ProvisionProgress(ByVal progress As ProvisionProgressEventType, ByVal projectName As String, ByVal message As String)
   Public Event ProvisionException(ByVal projectName As String, ex As Exception)

   Private myDTE As DTE2 = Nothing
   Private myTFS As TfsTeamProjectCollection = Nothing


   Public Sub Create(ByVal collectionUri As Uri, ByVal projectName As String, ByVal processTemplateName As String)
      Dim noAccounts(-1) As NTAccount
      Me.Create(collectionUri, projectName, processTemplateName, String.Empty, noAccounts)
   End Sub

   Public Sub Create(ByVal collectionUri As Uri, ByVal projectName As String, ByVal processTemplateName As String, groupName As String, groupAccounts As NTAccount())
      Me.Create(collectionUri, projectName, processTemplateName, groupName, groupAccounts, 3)
   End Sub

   Public Sub Create(ByVal collectionUri As Uri, ByVal projectName As String, ByVal processTemplateName As String, groupName As String, groupAccounts As NTAccount(), vsSpinUpWaitSeconds As Integer)
      Dim batchProjectFilespec As String = String.Empty
      Dim createLogFilespec As String = String.Empty

      Try
         'Spin up VS 2010 devenv
         If myDTE Is Nothing Then
            Const VS_PROG_ID As String = "VisualStudio.DTE.10.0"
            myDTE = DirectCast(Activator.CreateInstance(Type.GetTypeFromProgID(VS_PROG_ID), True), DTE2)
            If (myDTE Is Nothing) OrElse (myDTE.DTE Is Nothing) Then
               Throw New InvalidOperationException(VS_PROG_ID & " not available")
            End If

            'Wake up TFS
            myDTE.ExecuteCommand("View.TeamExplorer")
            RaiseEvent ProvisionProgress(ProvisionProgressEventType.ConnectToTfsClient, projectName, VS_PROG_ID)
         End If


         'Connect to TFS
         If (myTFS Is Nothing) OrElse (myTFS.Uri.AbsoluteUri.ToLower <> collectionUri.AbsoluteUri.ToLower) Then
            myTFS = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(collectionUri)
            RaiseEvent ProvisionProgress(ProvisionProgressEventType.ConnectToTfsServer, projectName, collectionUri.AbsoluteUri)
         End If


         'Begin project creation
         RaiseEvent ProvisionProgress(ProvisionProgressEventType.BeginProject, projectName, String.Empty)

         'Create XML project definition
         Dim project As New Project(collectionUri, projectName, processTemplateName)
         With project
            .LogFolder = Path.GetTempPath()
            .SccCreateType = sccCreateTypeEnum.New  'source control: YES
         End With

         batchProjectFilespec = Path.GetTempFileName()

         Dim xml As String = project.ToXML()
         File.WriteAllText(batchProjectFilespec, xml)

         RaiseEvent ProvisionProgress(ProvisionProgressEventType.CreateProjectDefinition, project.ProjectName, processTemplateName)


         'Create TFS project
         Dim tryCount As Integer = 3
BatchRetry:
         Try
            Thread.Sleep(vsSpinUpWaitSeconds * 1000)
            myDTE.ExecuteCommand("File.BatchNewTeamProject", batchProjectFilespec)
         Catch ex As Exception
            tryCount -= 1
            If tryCount > 0 Then
               GoTo BatchRetry  'some machines wakeup VS slower than others... :>)
            Else
               Throw
            End If
         End Try

         'Batch creation generates a log file with this name:
         createLogFilespec = Path.ChangeExtension(Path.Combine(project.LogFolder, project.ProjectName), ".log")


         'Verify the new project exists
         Dim vcAPI As VersionControlServer = CType(myTFS.GetService(GetType(VersionControlServer)), VersionControlServer)
         Dim tp As TeamProject = vcAPI.GetTeamProject(project.ProjectName)

         RaiseEvent ProvisionProgress(ProvisionProgressEventType.CreateTeamProject, tp.Name, tp.TeamProjectCollection.Uri.AbsoluteUri)


         'Setup initial project access
         If Not String.IsNullOrWhiteSpace(groupName) AndAlso groupAccounts.Count > 0 Then
            SetGroupMembership(tp, groupName, groupAccounts)
         End If


         'Cleanup
         File.Delete(batchProjectFilespec)
         File.Delete(createLogFilespec)

      Catch ex As Exception
         Dim message As String = "Project creation failed: " & ex.Message & Environment.NewLine & _
                                 "project:" & batchProjectFilespec & Environment.NewLine & _
                                 "log:" & createLogFilespec

         RaiseEvent ProvisionException(projectName, New Exception(message, ex))
      End Try
   End Sub

   Public Sub SetGroupMembership(collectionUri As Uri, projectName As String, groupName As String, groupAccounts As NTAccount())
      Try
         'Connect to TFS
         If (myTFS Is Nothing) OrElse (myTFS.Uri.AbsoluteUri.ToLower <> collectionUri.AbsoluteUri.ToLower) Then
            myTFS = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(collectionUri)
            RaiseEvent ProvisionProgress(ProvisionProgressEventType.ConnectToTfsServer, projectName, collectionUri.AbsoluteUri)
         End If

         Dim vcAPI As VersionControlServer = CType(myTFS.GetService(GetType(VersionControlServer)), VersionControlServer)
         Dim tp As TeamProject = vcAPI.GetTeamProject(projectName)

         SetGroupMembership(tp, groupName, groupAccounts)

      Catch ex As Exception
         Dim message As String = "Group membership failed: " & ex.Message & Environment.NewLine & _
                                 "project:" & projectName

         RaiseEvent ProvisionException(projectName, New Exception(message, ex))
      End Try
   End Sub

   Private Sub SetGroupMembership(tp As TeamProject, groupName As String, groupAccounts As NTAccount())
      Try
         Dim secAPI As IGroupSecurityService = CType(myTFS.GetService(GetType(IGroupSecurityService)), IGroupSecurityService)

         'create needed SIDs
         Dim groupSids As New Dictionary(Of String, SecurityIdentifier)
         For Each account As NTAccount In groupAccounts
            groupSids.Add(account.ToString, CType(account.Translate(GetType(SecurityIdentifier)), SecurityIdentifier))
         Next

         'spin thru all the project Groups
         Dim groupIds() As Identity = secAPI.ListApplicationGroups(tp.ArtifactUri.AbsoluteUri)
         For Each groupId As Identity In groupIds
            Dim group As Identity = secAPI.ReadIdentity(SearchFactor.Sid, groupId.Sid, QueryMembership.Direct)
            With group
               If .DisplayName = groupName Then
                  'remove any default/existing members
                  For Each memberSid As String In .Members
                     Dim member As Identity = secAPI.ReadIdentity(SearchFactor.Sid, memberSid, QueryMembership.Direct)

                     secAPI.RemoveMemberFromApplicationGroup(.Sid, memberSid)
                     RaiseEvent ProvisionProgress(ProvisionProgressEventType.RemoveGroupMember, tp.Name, member.AccountName & " (" & groupName & ")")
                  Next

                  'add the desired members
                  For Each groupSid As KeyValuePair(Of String, SecurityIdentifier) In groupSids
                     secAPI.AddMemberToApplicationGroup(.Sid, groupSid.Value.Value)
                     RaiseEvent ProvisionProgress(ProvisionProgressEventType.AddGroupMember, tp.Name, groupSid.Key & " (" & groupName & ")")
                  Next
               End If
            End With
         Next

      Catch ex As Exception
         Dim message As String = "Group membership failed: " & ex.Message & Environment.NewLine & _
                                 "project:" & tp.Name

         RaiseEvent ProvisionException(tp.Name, New Exception(message, ex))
      End Try
   End Sub
End Class
